Part1. 基础知识篇

Chapter2. 通过行为参数化传递代码

2.1 应对不断变化的需求

  • 在软件工程中,一个常见的问题就是用户的需求肯定会变。
  • 行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。这样,这个方法的行为就基于那块代码被参数化了。
  • 例如,有一个goAndExecute方法,这个方法接收一系列不同的新行为作为指令参数,然后去执行。

2.1.1 例子:筛选苹果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//一开始想筛选出绿色的苹果
public static List<Apple> filterGreenApples (List<Apple> inventory)
{
List<Apple> resultList = new ArrayList<>();
for(Apple apple : inventory)
{
if("green".equals(apple.getColor()))
{
resultList.add(apple);
}
}
return resultList;
}

//改需求了,还想要筛选红苹果,那么可以将颜色作为参数以应对变化
public static List<Apple> filterApplesByColor (List<Apple> inventory, String color)
{
List<Apple> resultList = new ArrayList<>();
for(Apple apple : inventory)
{
if("green".equals(color))
{
resultList.add(apple);
}
}
return resultList;
}

//又改需求了,想要区分轻苹果和重苹果,如以150g为标准区分,考虑到可能这个重量分界值会改变,可以如下编写:
public static List<Apple> filterApplesByWeight (List<Apple> inventory, int weight)
{
List<Apple> resultList = new ArrayList<>();
for(Apple apple : inventory)
{
if(apple.getWeight > weight)
{
resultList.add(apple);
}
}
return resultList;
}

//上面这样做有什么问题呢? 打破了DRY原则(Don`t Repeat Yoursel)

2.2 行为参数化

谓词

对选择标准建模,例如在上面的场景中,就是根据Apple的某些属性(颜色、重量)等来返回一个boolean值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface ApplePredicate 
{
boolean test (Apple apple);
}

public class AppleHeavyWeightPredicate implements ApplePredicate
{
public boolean test(Apple apple)
{
// 筛选出重苹果
return apple.getWeight() > 150;
}
}

public class AppleGreenColorPredicate implements ApplePredicate
{
public boolean test(Apple apple)
{
// 筛选出绿苹果
return "green".equals(apple.getColor());
}
}

策略模式

定义ApplePredicate算法族,实现了AppleHeavyWeightPredicate、AppleGreenColorPredicate等不同的策略算法

关键

如何利用ApplePredicate的不同实现?需要filterApples方法接收ApplePredicate对象,这就是行为参数化——让方法接收多种行为(或策略)作为参数,并在内部使用,来完成不同的行为。
使用:

1
2
3
4
5
6
7
8
9
10
11
12
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p)
{
List<Apple> resultList = new ArrayList<>();
for(Apple apple : inventory)
{
if(p.test(apple))
{
result.add(apple);
}
}
return resultList;
}

1.传递代码/行为

现在可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法,filterApples方法的行为取决于通过ApplePredicate对象传递的代码,即,把filterApples方法的行为参数化了!
不过,这种做法类似于在内联“传递代码”,因为是通过一个实现了test方法的对象来传递布尔表达式。

1
2
3
4
5
6
7
8
9
public class AppleRedAndHeavyPredicate implements ApplePredicate
{
public boolean test(Apple apple)
{
return "red".equals(apple.getColor()) && apple.getWeight() > 150;
}
}

List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());

2.多种行为,一个参数

行为参数化的好处在于可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来,这样可以重复使用同一个方法,给它不同的行为来达到不同的目的。
以不同格式输出苹果的信息的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface AppleFormatter
{
String accept(Apple apple);
}

public class AppleSimpleFormatter implements AppleFormatter
{
@Override
public String accept(Apple apple)
{
String characteristic = apple.getWeight() > 150 ? 'heavy' : 'light';
return "A " + characteristic + " " + apple.getColor() + " apple";
}
}

public class AppleFancyFormatter implements AppleFormatter
{
@Override
public String accept(Apple apple)
{
String characteristic = apple.getWeight() > 150 ? 'heavy' : 'light';
return "An apple of" + apple.getWeight() + " g";
}
}

public void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter)
{
for(Apple apple: inventory){
String output = formatter.accept(apple);
System.out.println(output);
}
}

2.3 对付啰嗦

1)匿名类
匿名类仍然比较笨重

1
2
3
4
5
6
7
List<Apple> redApples = filterApples(inventory, new ApplePredicate()
{
public boolean test(Apple apple)
{
return "red".equals(apple.getColor());
}
});

2)Lambda表达式

1
2
3
List<Apple> redApples = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
// 进一步省略:
List<Apple> redApples = filterApples(inventory, apple -> "red".equals(apple.getColor()));

3)抽象化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Predicate<T>{     
boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p)
{
List<T> result = new ArrayList<>();
for(T e: list)
{
if(p.test(e))
{
result.add(e);
}
}
return result;
}
// 现在可以把filter方法用在香蕉、橘子、Integer或是String的列表上了
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));

List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

2.4 真实的例子

  • 行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的不同谓词)将方法的行为参数化。
  • 这种做法类似于策略设计模式

2.4.1 用Comparator来排序

Java8中,List自带了一个sort方法(你也可以使用Collections.sort方法)。sort的行为可以用Java.util.Comparator对象来参数化,因此可以随时创建Comparator的实现,用sort方法表现出不同的行为。sort的接口如下:

1
2
3
4
5
// java.util.Comparator
public interface Comparator<T>
{
public int compare(T o1,T o2);
}

1
inventory.sort((apple1, apple2) -> apple1.getWeight().compareTo(apple2.getWeight()));

2.4.2 用Runnable执行代码块

在Java中,可以使用Runnable接口表示一个要执行的代码块,请注意,代码不会反悔任何结果,即void。

1
2
3
4
5
// java.lang.Runnable
public interface Runnable
{
public void run();
}

使用这个接口,创建执行不同行为的线程:

1
2
3
4
5
6
7
8
9
10
// 匿名类
Thread t = new Thread(new Runnable()
{
public void run()
{
System.out.println("Hello World");
}
});
// 使用Lambda表达式:
Thread t = new Thread(() -> System.out.println("Hello World"));

2.4.3 GUI事件处理

GUI编程的一个典型模式就是执行一个操作来响应特定事件,如鼠标点击,或在文字上悬停。

1
2
Button button = new Button("Send");
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));

2.5 小结

  • 行为参数化:一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力
  • 行为参数化可以让代码更好地适应不断变化的要求,减轻未来的工作量
  • 传递代码,就是将新行为作为参数传递给方法。
  • Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI事件处理等。